package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisClient;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import com.hmdp.utils.SystemConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedisClient redisClient;


    @Override
    public Result queryShopById(Long id) {
        //处理缓存穿透
        //return redisWithPassThrough(id);

        //互斥锁处理缓存击穿
        //return redisWithMutex(id);

        //逻辑过期处理缓存击穿
        //Shop shop =  redisClient.redisWithPassThrough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        Shop shop =  redisClient.redisWithLogicExpire(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, 2L, TimeUnit.MILLISECONDS);

        if(shop==null){
            return Result.fail("当前店铺不存在");
        }else {
            return Result.ok(shop);
        }

    }


    //创建线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    private Shop redisWithLogicExpire(Long id){
        //默认key-value被提前存储并且永远不会过期
        String shopJSON = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        if(StrUtil.isBlank(shopJSON)){
            return null;
        }

        //判断当前信息是否过期
        RedisData redisData = JSONUtil.toBean(shopJSON, RedisData.class);

        //转换数据类型
        JSONObject shopStr = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(shopStr, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        if(LocalDateTime.now().isAfter(expireTime)){
            //当前时间大于过期时间，尝试获取锁
            if(getKeyLock(id)){
                //获取成功

                    CACHE_REBUILD_EXECUTOR.submit(()->{

                        try {
                            Thread.sleep(200);
                            this.saveShop2Redis(id,20L);
                        }catch (Exception e){
                            e.printStackTrace();
                        }finally {
                            unLock(id);
                        }
                    });

            }
        }

        //返回当获取数据
        return shop;

    }
    public void saveShop2Redis(Long id, Long expireSeconds){
        //默认当前key不会过期
        Shop shop = getById(id);
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));

        //存储数据到redis中
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
    }


    //处理缓存穿透
    private Shop redisWithPassThrough(Long id) {
        String redisKey = RedisConstants.CACHE_SHOP_KEY + id;
        String shopInfoJSON = stringRedisTemplate.opsForValue().get(redisKey);

        Shop shop;
        //查询命中且不为""
        if (shopInfoJSON != null && !shopInfoJSON.isEmpty()) {
            //缓存中查询到了，直接返回
            shop = JSONUtil.toBean(shopInfoJSON, Shop.class);
            return shop;
        }
        //缓存命中，判断是否为空值: ""
        if (shopInfoJSON != null) {
            return null;
        }

        //查询数据库
        shop = getById(id);
        //判断if是否存在
        if (shop == null) {
            //shop为空，设置空对象
            stringRedisTemplate.opsForValue().set(redisKey, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }

        String shopStr = JSONUtil.toJsonStr(shop);
        //设置不同的随机TTL，防止缓存雪崩
        Random r = new Random();
        int ttl = Math.toIntExact(RedisConstants.CACHE_SHOP_TTL);
        ttl = r.nextInt(ttl) + 1;
        stringRedisTemplate.opsForValue().set(redisKey, shopStr, ttl, TimeUnit.MINUTES);

        return shop;
    }




    //互斥处理热点key/缓存击穿
    private Shop redisWithMutex(Long id) {
        String redisKey = RedisConstants.CACHE_SHOP_KEY + id;
        String shopInfoJSON = stringRedisTemplate.opsForValue().get(redisKey);

        Shop shop;
        //查询命中且不为""
        if (shopInfoJSON != null && !shopInfoJSON.isEmpty()) {
            //缓存中查询到了，直接返回
            shop = JSONUtil.toBean(shopInfoJSON, Shop.class);
            return shop;
        }
        //缓存命中，判断是否为空值: ""
        if (shopInfoJSON != null) {
            return null;
        }

        //缓存未命中，尝试获取锁
        try {
            while (true){
                if (getKeyLock(id)) {
                    //成功获取锁
                    shop = getById(id);
                    //判断shop是否存在，避免内存穿透
                    if (shop == null) {
                        //shop为空，设置空对象
                        stringRedisTemplate.opsForValue().set(redisKey, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                        return null;
                    }
                    String shopStr = JSONUtil.toJsonStr(shop);
                    stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,shopStr,RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES);
                    unLock(id);
                    return shop;
                }
                Thread.sleep(50);
                //没有得到锁，等待50ms后查看当前缓存中释放有数据
                String getShopAgain = stringRedisTemplate.opsForValue().get(redisKey);
                if(getShopAgain!=null && !getShopAgain.isEmpty()){
                    shop = JSONUtil.toBean(getShopAgain, Shop.class);
                    return shop;
                }
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            unLock(id);
        }

    }

    private boolean getKeyLock(Long id) {
        //尝试上锁
        Boolean getLock = stringRedisTemplate.opsForValue().setIfAbsent(RedisConstants.LOCK_SHOP_KEY + id, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(getLock);
    }

    private void unLock(Long id) {
        //释放锁
        stringRedisTemplate.delete(RedisConstants.LOCK_SHOP_KEY + id);
    }

    @Override
    @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("更新失败！店铺id不能为空");
        }
        //1.先更新数据库
        updateById(shop);
        //2.删除缓存
        stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + id);

        return Result.ok();
    }


    /**
     * 根据类型查询，并根据地理位置排序
     * @param typeId
     * @param current
     * @param x
     * @param y
     * @return
     */
    @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        //对坐标轴位置xy进行判断
        if(x==null || y==null){
            //不需要根据地理位置查询
            Page<Shop> page = query().eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));

            return Result.ok(page.getRecords());
        }

        //redis中的Key
        String key = "shop:geo:"+typeId;

        //计算分页参数
        int from = (current-1)* SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current*SystemConstants.DEFAULT_PAGE_SIZE;

        //在redis中查询 GEOSearch key bylonlat x,y by radius r withdist count n
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo().search(
                key,
                GeoReference.fromCoordinate(new Point(x, y)),
                new Distance(5000),
                //指定查询参数：返回结果包含距离，查询结果数量
                RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
        );

        if(results == null){
            return Result.ok(Collections.emptyList());
        }

        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = results.getContent();
        if(content.size()<=from){
            //没有下一页，返回
            return Result.ok(Collections.emptyList());
        }

        List<Integer> ids = new ArrayList<>();
        Map<String, Double> map = new HashMap<>();
        content.stream().skip(from).forEach(result ->{
            String shopId = result.getContent().getName();
            ids.add(Integer.valueOf(shopId));

            double value = result.getDistance().getValue();
            map.put(shopId, value);
        });

        String idsStr = StrUtil.join(",", ids);
        List<Shop> shops = query().in("id", ids).last("order by field(id," + idsStr + ")").list();

        shops.forEach(shop -> {
            shop.setDistance(map.get(shop.getId().toString()));
        });

        return Result.ok(shops);
    }
}











